
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.chimpchat;

import com.google.common.collect.Lists;

import com.android.chimpchat.core.PhysicalButton;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Collection;
import java.util.Collections;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Provides a nicer interface to interacting with the low-level network access protocol for talking
 * to the monkey.
 * <p>
 * This class is thread-safe and can handle being called from multiple threads.
 */
public class ChimpManager {
  private static Logger LOG = Logger.getLogger(ChimpManager.class.getName());

  private Socket monkeySocket;
  private BufferedWriter monkeyWriter;
  private BufferedReader monkeyReader;

  /**
   * Create a new ChimpMananger to talk to the specified device.
   *
   * @param monkeySocket the already connected socket on which to send protocol messages.
   * @throws IOException if there is an issue setting up the sockets
   */
  public ChimpManager(Socket monkeySocket) throws IOException {
    this.monkeySocket = monkeySocket;
    monkeyWriter =
        new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream()));
    monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream()));
  }

  /**
   * Send a touch down event at the specified location.
   *
   * @param x the x coordinate of where to click
   * @param y the y coordinate of where to click
   * @return success or not
   * @throws IOException on error communicating with the device
   */
  public boolean touchDown(int x, int y) throws IOException {
    return sendMonkeyEvent("touch down " + x + " " + y);
  }

  /**
   * Send a touch down event at the specified location.
   *
   * @param x the x coordinate of where to click
   * @param y the y coordinate of where to click
   * @return success or not
   * @throws IOException on error communicating with the device
   */
  public boolean touchUp(int x, int y) throws IOException {
    return sendMonkeyEvent("touch up " + x + " " + y);
  }

  /**
   * Send a touch move event at the specified location.
   *
   * @param x the x coordinate of where to click
   * @param y the y coordinate of where to click
   * @return success or not
   * @throws IOException on error communicating with the device
   */
  public boolean touchMove(int x, int y) throws IOException {
    return sendMonkeyEvent("touch move " + x + " " + y);
  }

  /**
   * Send a touch (down and then up) event at the specified location.
   *
   * @param x the x coordinate of where to click
   * @param y the y coordinate of where to click
   * @return success or not
   * @throws IOException on error communicating with the device
   */
  public boolean touch(int x, int y) throws IOException {
    return sendMonkeyEvent("tap " + x + " " + y);
  }

  /**
   * Press a physical button on the device.
   *
   * @param name the name of the button (As specified in the protocol)
   * @return success or not
   * @throws IOException on error communicating with the device
   */
  public boolean press(String name) throws IOException {
    return sendMonkeyEvent("press " + name);
  }

  /**
   * Send a Key Down event for the specified button.
   *
   * @param name the name of the button (As specified in the protocol)
   * @return success or not
   * @throws IOException on error communicating with the device
   */
  public boolean keyDown(String name) throws IOException {
    return sendMonkeyEvent("key down " + name);
  }

  /**
   * Send a Key Up event for the specified button.
   *
   * @param name the name of the button (As specified in the protocol)
   * @return success or not
   * @throws IOException on error communicating with the device
   */
  public boolean keyUp(String name) throws IOException {
    return sendMonkeyEvent("key up " + name);
  }

  /**
   * Press a physical button on the device.
   *
   * @param button the button to press
   * @return success or not
   * @throws IOException on error communicating with the device
   */
  public boolean press(PhysicalButton button) throws IOException {
    return press(button.getKeyName());
  }

  /**
   * This function allows the communication bridge between the host and the device
   * to be invisible to the script for internal needs.
   * It splits a command into monkey events and waits for responses for each over an adb tcp socket.
   * Returns on an error, else continues and sets up last response.
   *
   * @param command the monkey command to send to the device
   * @return the (unparsed) response returned from the monkey.
   */
  private String sendMonkeyEventAndGetResponse(String command) throws IOException {
    command = command.trim();
    LOG.info("Monkey Command: " + command + ".");

    // send a single command and get the response
    monkeyWriter.write(command + "\n");
    monkeyWriter.flush();
    return monkeyReader.readLine();
  }

  /**
   * Parse a monkey response string to see if the command succeeded or not.
   *
   * @param monkeyResponse the response
   * @return true if response code indicated success.
   */
  private boolean parseResponseForSuccess(String monkeyResponse) {
    if (monkeyResponse == null) {
      return false;
    }
    // return on ok
    if (monkeyResponse.startsWith("OK")) {
      return true;
    }

    return false;
  }

  /**
   * Parse a monkey response string to get the extra data returned.
   *
   * @param monkeyResponse the response
   * @return any extra data that was returned, or empty string if there was nothing.
   */
  private String parseResponseForExtra(String monkeyResponse) {
    int offset = monkeyResponse.indexOf(':');
    if (offset < 0) {
      return "";
    }
    return monkeyResponse.substring(offset + 1);
  }

  /**
   * This function allows the communication bridge between the host and the device
   * to be invisible to the script for internal needs.
   * It splits a command into monkey events and waits for responses for each over an
   * adb tcp socket.
   *
   * @param command the monkey command to send to the device
   * @return true on success.
   */
  public boolean sendMonkeyEvent(String command) throws IOException {
    synchronized (this) {
      String monkeyResponse = sendMonkeyEventAndGetResponse(command);
      return parseResponseForSuccess(monkeyResponse);
    }
  }

  /**
   * Close all open resources related to this device.
   */
  public void close() {
    try {
      monkeySocket.close();
    } catch (IOException e) {
      LOG.log(Level.SEVERE, "Unable to close monkeySocket", e);
    }
    try {
      monkeyReader.close();
    } catch (IOException e) {
      LOG.log(Level.SEVERE, "Unable to close monkeyReader", e);
    }
    try {
      monkeyWriter.close();
    } catch (IOException e) {
      LOG.log(Level.SEVERE, "Unable to close monkeyWriter", e);
    }
  }

  /**
   * Function to get a static variable from the device.
   *
   * @param name name of static variable to get
   * @return the value of the variable, or null if there was an error
   */
  public String getVariable(String name) throws IOException {
    synchronized (this) {
      String response = sendMonkeyEventAndGetResponse("getvar " + name);
      if (!parseResponseForSuccess(response)) {
        return null;
      }
      return parseResponseForExtra(response);
    }
  }

  /**
   * Function to get the list of static variables from the device.
   */
  public Collection<String> listVariable() throws IOException {
    synchronized (this) {
      String response = sendMonkeyEventAndGetResponse("listvar");
      if (!parseResponseForSuccess(response)) {
        Collections.emptyList();
      }
      String extras = parseResponseForExtra(response);
      return Lists.newArrayList(extras.split(" "));
    }
  }

  /**
   * Tells the monkey that we are done for this session.
   *
   * @throws IOException
   */
  public void done() throws IOException {
    // this command just drops the connection, so handle it here
    synchronized (this) {
      sendMonkeyEventAndGetResponse("done");
    }
  }

  /**
   * Tells the monkey that we are done forever.
   *
   * @throws IOException
   */
  public void quit() throws IOException {
    // this command drops the connection, so handle it here
    synchronized (this) {
      sendMonkeyEventAndGetResponse("quit");
    }
  }

  /**
   * Send a tap event at the specified location.
   *
   * @param x the x coordinate of where to click
   * @param y the y coordinate of where to click
   * @return success or not
   * @throws IOException
   * @throws IOException on error communicating with the device
   */
  public boolean tap(int x, int y) throws IOException {
    return sendMonkeyEvent("tap " + x + " " + y);
  }

  /**
   * Type the following string to the monkey.
   *
   * @param text the string to type
   * @return success
   * @throws IOException
   */
  public boolean type(String text) throws IOException {
    // The network protocol can't handle embedded line breaks, so we have to handle it
    // here instead
    StringTokenizer tok = new StringTokenizer(text, "\n", true);
    while (tok.hasMoreTokens()) {
      String line = tok.nextToken();
      if ("\n".equals(line)) {
        boolean success = press(PhysicalButton.ENTER);
        if (!success) {
          return false;
        }
      } else {
        boolean success = sendMonkeyEvent("type " + line);
        if (!success) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Type the character to the monkey.
   *
   * @param keyChar the character to type.
   * @return success
   * @throws IOException
   */
  public boolean type(char keyChar) throws IOException {
    return type(Character.toString(keyChar));
  }

  /**
   * Wake the device up from sleep.
   *
   * @throws IOException
   */
  public void wake() throws IOException {
    sendMonkeyEvent("wake");
  }
}
